// Copyright 2010 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package com.apchernykh.rozhlasapp.utils;

import org.apache.http.Header;
import org.apache.http.HttpRequest;
import org.apache.http.HttpResponse;
import org.apache.http.HttpResponseFactory;
import org.apache.http.ParseException;
import org.apache.http.ProtocolVersion;
import org.apache.http.StatusLine;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.conn.ClientConnectionOperator;
import org.apache.http.conn.OperatedClientConnection;
import org.apache.http.conn.scheme.PlainSocketFactory;
import org.apache.http.conn.scheme.Scheme;
import org.apache.http.conn.scheme.SchemeRegistry;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.impl.conn.DefaultClientConnection;
import org.apache.http.impl.conn.DefaultClientConnectionOperator;
import org.apache.http.impl.conn.DefaultResponseParser;
import org.apache.http.impl.conn.SingleClientConnManager;
import org.apache.http.io.HttpMessageParser;
import org.apache.http.io.SessionInputBuffer;
import org.apache.http.message.BasicHttpRequest;
import org.apache.http.message.BasicHttpResponse;
import org.apache.http.message.BasicLineParser;
import org.apache.http.message.ParserCursor;
import org.apache.http.params.HttpParams;
import org.apache.http.protocol.HTTP;
import org.apache.http.util.CharArrayBuffer;

import com.apchernykh.rozhlasapp.interfaces.IProxyErrorsListener;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketTimeoutException;
import java.net.UnknownHostException;
import java.util.StringTokenizer;

public class StreamProxy implements Runnable {

	private int mPort = 0;
	final IProxyErrorsListener mErrorsListener;

	public StreamProxy(IProxyErrorsListener iParent) {
		mErrorsListener = iParent;
	}

	public int getPort() {
		return mPort;
	}

	private boolean mIsRunning = true;
	private ServerSocket mSocket;
	private Thread mThread;

	// We expose this member for testing sudden close();
	// So, that the test stub could just call mData.close()
	public InputStream mData;

	public void init() {
		try {
			mSocket = new ServerSocket(mPort, 0,
					InetAddress.getByAddress(new byte[] { 127, 0, 0, 1 }));
			mSocket.setSoTimeout(1000);
			mPort = mSocket.getLocalPort();
			ConditionalLog.d("port " + mPort + " obtained");
		} catch (UnknownHostException e) {
			ConditionalLog.e("Error initializing server", e);
		} catch (IOException e) {
			ConditionalLog.e("Error initializing server", e);
		}
		if (mPort == 0) {
			mErrorsListener.onProxyError(" port = 0");
		}
	}

	public void start() {

		if (mSocket == null) {
			mErrorsListener.onProxyError(" socket = null ");
			throw new IllegalStateException(
					"Cannot start proxy; it has not been initialized.");
		}

		mThread = new Thread(this);
		mThread.start();
	}

	public void stop() {
		mIsRunning = false;

		if (mThread == null) {
			throw new IllegalStateException(
					"Cannot stop proxy; it has not been started.");
		}

		//we want to interrupt only if stop() was called from separate thread.
		//Otherwise, isRunning=false is enough
		if (Thread.currentThread() != mThread) {
			mThread.interrupt();
			try {
				mThread.join(1000);
			} catch (InterruptedException e) {
				ConditionalLog.e(" stop failed ", e);
				mErrorsListener.onProxyError(" stop failed ");
			}
		}
	}

	@Override
	public void run() {
		ConditionalLog.d("running");
		while (mIsRunning) {
			try {
				Socket aClient = mSocket.accept();
				if (aClient == null) {
					continue;
				}
				ConditionalLog.d("client connected");
				HttpRequest aRequest = readRequest(aClient);
				processRequest(aRequest, aClient);
			} catch (SocketTimeoutException e) {
				ConditionalLog.e(" run failed ", e);
				mErrorsListener.onProxyError(" run failed ");
				mIsRunning = false;
				// Do nothing
			} catch (IOException e) {
				ConditionalLog.e("IOError connecting to client", e);
				mErrorsListener.onProxyError("IOError connecting to client");
				mIsRunning = false;
			}
		}
		ConditionalLog.e("Proxy interrupted. Shutting down.");
	}

	private HttpRequest readRequest(Socket aClient) {
		HttpRequest aRequest = null;
		InputStream aInputStream;
		String aFirstLine;
		try {
			aInputStream = aClient.getInputStream();
			BufferedReader aReader = new BufferedReader(
					new InputStreamReader(aInputStream), 8192);
			aFirstLine = aReader.readLine();
		} catch (IOException e) {
			ConditionalLog.e("IOError parsing request", e);
			mErrorsListener.onProxyError("IOError parsing request");
			mIsRunning = false;
			return aRequest;
		}

		if (aFirstLine == null) {
			ConditionalLog
					.e("Proxy client closed connection without a request.");
			mErrorsListener
					.onProxyError("Proxy client closed connection without a request.");
			mIsRunning = false;
			return aRequest;
		}

		StringTokenizer aTokenizer = new StringTokenizer(aFirstLine);
		String aMethod = aTokenizer.nextToken();
		String aUri = aTokenizer.nextToken();
		ConditionalLog.d(aUri);
		String aRealUri = aUri.substring(1);
		ConditionalLog.d(aRealUri);
		aRequest = new BasicHttpRequest(aMethod, aRealUri);
		return aRequest;
	}

	private HttpResponse download(final String iUrl) {
		DefaultHttpClient aSeed = new DefaultHttpClient();
		SchemeRegistry aRegistry = new SchemeRegistry();
		aRegistry.register(new Scheme("http", PlainSocketFactory
				.getSocketFactory(), 80));
		SingleClientConnManager aMgr = new MyClientConnManager(aSeed.getParams(),
				aRegistry);
		DefaultHttpClient aHttpClient = new DefaultHttpClient(aMgr, aSeed.getParams());
		HttpGet aHttpMethod = new HttpGet(iUrl);
		HttpResponse aHttpResponse = null;
		try {
			ConditionalLog.d("starting download");
			aHttpResponse = aHttpClient.execute(aHttpMethod);
			ConditionalLog.d("downloaded");
		} catch (ClientProtocolException e) {
			ConditionalLog.e("Error downloading", e);
		} catch (IOException e) {
			ConditionalLog.e("Error downloading", e);
		}
		return aHttpResponse;
	}

	private void processRequest(HttpRequest iHttpRequest, Socket iClientSocket)
			throws IllegalStateException, IOException {
		if (iHttpRequest == null) {
			return;
		}
		ConditionalLog.d("processing");
		String aRequestUri = iHttpRequest.getRequestLine().getUri();
		HttpResponse aRealHttpResponse = download(aRequestUri);
		if (aRealHttpResponse == null) {
			mErrorsListener.onProxyError("realResponse == null");
			mIsRunning = false;
			return;
		}

		ConditionalLog.d("downloading...");

		mData = aRealHttpResponse.getEntity().getContent();
		StatusLine aStatusLine = aRealHttpResponse.getStatusLine();
		HttpResponse aHttpResponse = new BasicHttpResponse(aStatusLine);
		aHttpResponse.setHeaders(aRealHttpResponse.getAllHeaders());

		ConditionalLog.d("reading headers");
		StringBuilder aHttpString = new StringBuilder();
		aHttpString.append(aHttpResponse.getStatusLine().toString());

		aHttpString.append("\n");
		for (Header aCurrentHeader : aHttpResponse.getAllHeaders()) {
			aHttpString.append(aCurrentHeader.getName()).append(": ").append(aCurrentHeader.getValue())
					.append("\n");
		}
		aHttpString.append("\n");
		ConditionalLog.d("headers done");

		try {
			byte[] aBuffer = aHttpString.toString().getBytes();
			int aReadBytes;
			ConditionalLog.d("writing to client");
			iClientSocket.getOutputStream().write(aBuffer, 0, aBuffer.length);

			// Start streaming content.
			byte[] aBuff = new byte[1024 * 50];
			while (mIsRunning
					&& (aReadBytes = mData.read(aBuff, 0, aBuff.length)) != -1) {
				iClientSocket.getOutputStream().write(aBuff, 0, aReadBytes);
			}
		} catch (Exception e) {
			mErrorsListener.onProxyError("??? error on processrequest");
			ConditionalLog.e(e.getMessage(), e);
			mIsRunning = false;
		} finally {
			if (mData != null) {
				mData.close();
			}
			iClientSocket.close();
		}
	}

	private class IcyLineParser extends BasicLineParser {
		private static final String ICY_PROTOCOL_NAME = "ICY";

		private IcyLineParser() {
			super();
		}

		@Override
		public boolean hasProtocolVersion(CharArrayBuffer aBuffer,
				ParserCursor aCursor) {
			boolean aSuperFound = super.hasProtocolVersion(aBuffer, aCursor);
			if (aSuperFound) {
				return true;
			}
			int aIndex = aCursor.getPos();

			final int aProtolLength = ICY_PROTOCOL_NAME.length();

			if (aBuffer.length() < aProtolLength)
				return false; // not long enough for "HTTP/1.1"

			if (aIndex < 0) {
				// end of line, no tolerance for trailing whitespace
				// this works only for single-digit major and minor version
				aIndex = aBuffer.length() - aProtolLength;
			} else if (aIndex == 0) {
				// beginning of line, tolerate leading whitespace
				while ((aIndex < aBuffer.length())
						&& HTTP.isWhitespace(aBuffer.charAt(aIndex))) {
					aIndex++;
				}
			} // else within line, don't tolerate whitespace

			return aIndex + aProtolLength <= aBuffer.length()
					&& aBuffer.substring(aIndex, aIndex + aProtolLength).equals(
							ICY_PROTOCOL_NAME);

		}

		@Override
		public ProtocolVersion parseProtocolVersion(CharArrayBuffer aBuffer,
				ParserCursor aCursor) throws ParseException {

			if (aBuffer == null) {
				throw new IllegalArgumentException(
						"Char array buffer may not be null");
			}
			if (aCursor == null) {
				throw new IllegalArgumentException(
						"Parser cursor may not be null");
			}

			final int aProtolength = ICY_PROTOCOL_NAME.length();

			int aIndexFrom = aCursor.getPos();
			int aIndexTo = aCursor.getUpperBound();

			skipWhitespace(aBuffer, aCursor);

			int aIndex = aCursor.getPos();

			// long enough for "HTTP/1.1"?
			if (aIndex + aProtolength + 4 > aIndexTo) {
				throw new ParseException("Not a valid protocol version: "
						+ aBuffer.substring(aIndexFrom, aIndexTo));
			}

			// check the protocol name and slash
			if (!aBuffer.substring(aIndex, aIndex + aProtolength).equals(ICY_PROTOCOL_NAME)) {
				return super.parseProtocolVersion(aBuffer, aCursor);
			}

			aCursor.updatePos(aIndex + aProtolength);

			return createProtocolVersion(1, 0);
		}

		@Override
		public StatusLine parseStatusLine(CharArrayBuffer aBuffer,
				ParserCursor aCursor) throws ParseException {
			return super.parseStatusLine(aBuffer, aCursor);
		}
	}

	class MyClientConnection extends DefaultClientConnection {
		@Override
		protected HttpMessageParser createResponseParser(
				final SessionInputBuffer aBuffer,
				final HttpResponseFactory aResponseFactory,
				final HttpParams aParams) {
			return new DefaultResponseParser(aBuffer, new IcyLineParser(),
					aResponseFactory, aParams);
		}
	}

	class MyClientConnectionOperator extends DefaultClientConnectionOperator {
		public MyClientConnectionOperator(final SchemeRegistry iSchemeRegistry) {
			super(iSchemeRegistry);
		}

		@Override
		public OperatedClientConnection createConnection() {
			return new MyClientConnection();
		}
	}

	class MyClientConnManager extends SingleClientConnManager {
		private MyClientConnManager(HttpParams iHttpParams, SchemeRegistry iSchreg) {
			super(iHttpParams, iSchreg);
		}

		@Override
		protected ClientConnectionOperator createConnectionOperator(
				final SchemeRegistry iSchreg) {
			return new MyClientConnectionOperator(iSchreg);
		}
	}

}
